Hito 3 - Clasificadores¶

ANEXOS:

  • clasificador1_en
  • clasificador1_es
  • clasificador2_BERTweet_en
  • clasificador2_transformer_en
  • clasificador2_transformer_es

Para cada idioma, ¿somos capaces de ajustar un modelo predictivo que reciba un tweet y prediga su emoji asociado?¶

Las herramientas de procesamiento de texto natural han mostrado capacidades muy parecidas a las humanas. Testear su potencial en el contexto de este dataset es interesante puesto a que la variable a predecir es inherentemente subjetiva. En general, se espera que el emoji esté asociado al carácter emocional del tweet en cuestión, por ende tiene sentido testear modelos que han sido entrenados o ajustados para detectar sentimientos. No obstante, en este desafío hay emojis que presentan similar valor emocional. Además puede ser que el emoji corresponda a variables de mayor complejidad, como el sarcasmo del mensaje. Es por esto que el éxito en la predicción sería tarea difícil incluso para un humano.

Para responder a esta pregunta podemos usar modelos como Naïve Bayes, en el cual tomamos en consideración la ocurrencia de cada palabra en tweets de cada emoji, información que luego se usa para generar una probabilidad de emoji dado el tweet. También podría ser interesante usar modelos que tomen en consideración la interacción entre palabras. Un ejemplo de esto son los modelos de lenguaje. Podemos usar modelos de lenguajes pre-entrenados basados en redes neuronales, como es el caso de BERT/BETO, y ajustarlos para la predicción de emojis.

Propuesta metodológica¶

Para responder a esta pregunta queremos usar distintos métodos de clasificación. Puede que algunos tengan más éxito que otros y es de nuestro interés analizar por qué, de ser el caso.

Como clasificadores hay muchos, usaremos los los de la lista siguiente:

  • Naïve Bayes (1)
  • Clasificadores basados en Transformers

Antes que preferir una lista extensa de métodos, queremos analizar adecuadamente cada uno de ellos. Además, como este desafío se enmarcó en la competencia SEMEVAL, contamos con una extensa lista de competidores que incluye sus métricas globales de clasificación. Podemos, en la mayoría de los casos, averiguar qué método usaron. De este modo tendremos un análisis global del uso de diferentes métodos para clasificación multimodal de texto.

Por qué y como usar Naïve Bayes?¶

Creemos que este método, pese a su simpleza, puede dar resultados interesantes en esta tarea. Como ejemplo consideremos el siguiente tweet:

Nearly halfway to Christmas 🎄 Let me know, what's your favorite thing about winter? Share this post with someone who celebrates Christmas all year round! 🎄

Este tweet está relacionado con navidad, lo cual es evidente gracias a la presencia de ciertas palabras como: christmas y winter. Como consecuencia, está muy propenso a que la clase en cuestión sea aquella del emoji _christmastree, lo cual es efectivmente el caso. Si bien esto es menos claro para otros emoji, podemos generalizar esta idea y asumir que la clase del tweet estará dada por las palabras que lo componen. A su vez, cada palabra tendrá una probabilidad de pertenecer a las clases en cuestión.

El procedimiento para el clasificador será el siguiente:

Dado un parámetro alfa y un vocabulario, ajustamos un clasificador Naïve Bayes en base al conjunto de entrenamiento. Luego testeados la calidad de su evaluación con diferentes métricas usando el conjuntos de prueba (test).

Una razón para el uso de este método es que los resultados de Naïve Bayes son altamente interpretables puesto a que a cada palabra se le asigna la probabilidad de pertenecer a las distintas clases. Esto nos da un eje extra de exploración que usaremos del siguiente modo, dado un clasificador entrenado :

Para cada emoji, seleccionar k palabras con probabilidad más alta de ser catalogadas con el emoji.

Otra manera de interpretar los resultados del clasificador es el siguiente: para cada palabra poseemos la probabilidad de pertenecer a alguna clase (emoji). Como tenemos 20 emojis (19 respectivamente), entonces esto nos dota de un vector 20-dimensional (resp. 19-d) a valores en [0,1]. Esto nos permite usar alguna técnica de reducción de dimensionalidad para visualizar el espacio que se genera con tales representaciones. Para esta tarea elegimos umap, pues algunos de los miembros del grupo ya tienen vasta experiencia usándola y afinando sus hiperparámetros. En resumen realizamos lo siguiente:

Para cada palabra del vocabulario, tomar su vector n-dimensional dado por la probabilidad de pertenecer a las n clases. Realizar una reducción de dimensión usando UMAP y proyectalos en el plano junto con el color de la clase más probable y con tamaño del punto dependiendo de cuan fuerte es la probabilidad de pertenecer a su clase más probable. Realizar un análisis cualitativo.

Para la implementación del clasificador usaremos la librería scikitlearn. Un parámetro a ajustar es el alpha. Este corresponde a la suavización de la verosimilitud, que está dada por la ecuación siguiente:

$$ \theta_{y^i} = \frac{N_{y^i}+\alpha}{N_y+\alpha n} $$

Donde $N_{y^i}$ es el número de veces que la palabra $i$ aparece en la clase $y$, y $N_y$ es el conteo total de palabras para la clase $y$. El valor $\theta_{y^i}$ corresponde a la probabilidad de que una palabra $i$ aparezca en la clase (emoji) $y$.

Por otro lado, la definición del vocabulario es importante a la hora de usar NB. Usamos también la librería scikitlearn para esto. Esta posee un parámetro min_df, que corresponde a la mínima cantidad de ocurrencia que debe tener una palabra para que está sea tomada en cuenta en el clasificador. De este modo, un min_df = 1 tomara todas las palabras. Usar un min_df más elevado nos permitirá analizar solo aquellas palabras que suceden seguido y por ende que portarán más información acerca de la pertenencia o no a una cierta clase.

Realizaremos un grid search para ajustar ambos parámetros en nuestro clasificador. Exploraremos los valores siguientes:

$$\alpha \in \text{[}0.0,0.2,0.4,0.6,0.8,1.0\text{]}$$$$min\_ df \in \text{[} 1,2,3,4,5,6,7,8,9,10 \text{]} $$

Y escogeremos el ganador en base a la métrica macro f1 para ser consistentes con el resultado de la competición SEMEVAL.

Para distintos valores de alpha y min_df, entrenar un clasificador NB y escoger aquel con mayor macro f1

Por qué y como usar Transformers?¶

Transformers [2] es una arquitectura de redes neuronales que ha mostrado una enorme capacidad de modelar texto. Su uso se ha masificado y simplificado gracias a la existencia de bibliotecas como transformers, que ponen a libre disposición modelos pre-entrenados. Este punto es importante puesto a que entrenar un modelo de lenguaje robusto desde cero puede tomar tiempo y capacidad de cómputo que van más allá de nuestras capacidades.

Creemos que explorar su uso y compararlo con un clasificador como Naïve Bayes será una buena manera de complementar los conocimientos adquiridos en el curso con recursos más avanzados de interés personal. Sin embargo nos centraremos en modelos pre-entrenados sin ajustar. En particular analizaremos los siguientes:

  • BERTweet base (vinai) (3)
  • BERTweet ajustado para predicción de emojis (CardiffNLP)
  • Twitter-roberta base (CardiffNLP) (4)
  • Twitter-roberta ajustado para predicción de emojis (CardiffNLP)
  • BETO base (DCC-UChile) (5)

Esta eleccion nos permitirá comparar el efecto de hacer un ajuste a la tarea de predicción de emojis para dos modelos de transformers distintos en inglés. Por otro lado, testearemos el uso de un modelo de lenguaje sin ajustar para los tweets en español. Se hará un análisis de las predicciones consistente con el que usamos en Naïve Bayes, haciendo uso de las métricas de clasificación que provee la biblioteca scikitlearn.

Por otro lado, se buscarán modos de interpretación al análizar algunas capas de atención de los modelos. Para esto usaremos la herramienta open source bertviz (6) que nos provee de una herramienta de visualización interactiva.

Finalmente, cabe señalar que hay una conexión directa con la pregunta 2, que pretende analizar clusters usando distintas representaciones de los tweets. En efecto, los modelos de transformers anteriormente mencionados nos otorgan un vector por tweet que puede será usado con este efecto. De este modo estaremos explorando también el clasificador en cuestión a través del espacio semántico que define.

Métricas de evaluación general clasificadores¶

Independiente del método de clasificación a usar, estos serán evaluados con todas las métricas disponibles en el reporte de clasificación de scikit-learn:

  • f1-score
  • precision
  • recall
  • accuracy

Más aún, se observarán los valores de las tres primeras para cada uno de los emojis en particular. Tendremos acceso tanto a los promedios macro y weighted de para f1-score, precision y recall, sin embargo se prestará mayor atención a los valores macro, puesto a que weighted pondera cada resultado por la frecuencia de la clase en cuestión. Esto es problemático en nuestro caso, pues como pudimos observar en la exploración de datos, el dataset está extremadamente desbalanceado y por ende estaríamos asignando excesiva importancia a aquellos emoji de frecuencia alta como ❤.

Para concluir respecto a cual clasificador es mejor en general y para realizar grid-search (Naive-Bayes), se usará la métrica macro f1. Esto tiene dos principales justificaciones. La primera es que esto es consistente con la competencia original que introduce este dataset. Por otro lado, esta métrica involucra tanto recall como precisión. En nuestro caso la tarea no pone más énfasis en la sensibilidad o la precisión que deba tener el clasificador, luego la razón de usar una combinación de ambos, que es el caso de f1.

Referencias

[1] Metsis, V., Androutsopoulos, I., & Paliouras, G. (2006, July). Spam filtering with naive bayes-which naive bayes?. In CEAS (Vol. 17, pp. 28-69).

[2] Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., & Polosukhin, I. (2017). Attention is All you Need. Advances in Neural Information Processing Systems, 30, 5998–6008. https://arxiv.org/abs/1706.03762

[3] Nguyen, D. Q., Vu, T., & Nguyen, A. T. (2020, October). BERTweet: A pre-trained language model for English Tweets. In Proceedings of the 2020 Conference on Empirical Methods in Natural Language Processing: System Demonstrations (pp. 9-14). https://aclanthology.org/2020.emnlp-demos.2.pdf

[4] Barbieri, F., Camacho-Collados, J., Anke, L. E., & Neves, L. (2020, November). TweetEval: Unified Benchmark and Comparative Evaluation for Tweet Classification. In Findings of the Association for Computational Linguistics: EMNLP 2020 (pp. 1644-1650). https://arxiv.org/pdf/2010.12421.pdf

[5] Canete, J., Chaperon, G., Fuentes, R., Ho, J. H., Kang, H., & Pérez, J. (2020). Spanish pre-trained bert model and evaluation data. Pml4dc at iclr, 2020, 2020. https://users.dcc.uchile.cl/~jperez/papers/pml4dc2020.pdf

[6] Vig, J. (2019, July). A Multiscale Visualization of Attention in the Transformer Model. In Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics: System Demonstrations (pp. 37-42). https://aclanthology.org/P19-3007.pdf

Implementación¶

Naive Bayes en Inglés¶

Como se explicó en la planificación, para determinar los parámetros apropiados a utilizar en Naive-Bayes respecto a alpha y al mínimo número de ocurrencias de token a usa, se aplicó un grid search.

Este último parámetro fue variado al usar el método CountVectorizer, que es un vectorizador que tiene un parámetro $min\_df$, que corresponde al mínimo de apariciones que debe tener un token para ser incluido en el vocabulario. Esto nos da un método para incluir o no palabras poco frecuentes y que potencialmente empujarían a una cierta clase sin necesariamente ser términos característicos de esta.

Se procedió entonces a buscar aquel par de parámetros $df_min$ y $\alpha$ que generen una mejor clasificación en el conjunto de entrenamiento. Como se explicó en la propuesta metodológica, usaremos los siguientes candidatos a valores para $df\_min$: $$ df\_min = [1,2,3,4,5,6,7,8,9,10] $$ Notemos que con $df\_min=1$ estamos tomando en cuenta todos los tokens, mientras que con $df\_min$ igual a $k$ equivale a no tomar en cuenta los tokens que sucedan menos de $k$ veces en el conjunto de entrenamiento. Por otro lado, $\alpha\in [0,1]$, parámetro para el cual consideraremos una grilla con paso $0.2$.

Si bien en la secciones clasificafor1_en y clasificador1_es detallamos los resultados usando tanto accuracy como weighted f1, la deicisón final se tomó con macro f1 (siempre en el conjunto de test). Esto se muestra en el siguiente mapa de calor, donde mientras más clara sea la celda mayor es el macro f1 y por ende mejor el resultado.

gridsearch

Parámetros escogidos:

  • alpha = 0.2
  • minimas ocurrencias para token = 10

Resultados de clasificación:

  • accuracy = 0.30642
  • macro f1 = 0.22695113269450196
  • weighted f1 = 0.2876509870746095
In [ ]:
%reload_ext autoreload
%autoreload 2
from config_p1 import *

df_us_train = pickle.load(open(file_names["df_us_train"], "rb"))
df_us_test = pickle.load(open(file_names["df_us_test"], "rb"))
In [ ]:
from nltk.tokenize import TweetTokenizer
tt = TweetTokenizer()

df_us_train['tokenized_text'] = df_us_train['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
df_us_test['tokenized_text'] = df_us_test['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
In [ ]:
best_min_df = 10
best_alpha = 0.2

vectorizer = CountVectorizer(min_df=best_min_df)
X_train_bow = vectorizer.fit_transform(df_us_train["tokenized_text"])
X_test_bow = vectorizer.transform(df_us_test["tokenized_text"])

clf = MultinomialNB(alpha=best_alpha)
clf.fit(X_train_bow, df_us_train["label"])

y_pred_NB = clf.predict(X_test_bow)
dict_NB = classification_report(df_us_test["label"], y_pred_NB, target_names=df_us_mapping["emoji"],output_dict=True)
In [ ]:
print(classification_report(df_us_test["label"], y_pred_NB, target_names=df_us_mapping["emoji"]))
              precision    recall  f1-score   support

           ❤       0.38      0.48      0.42     10798
           😍       0.26      0.24      0.25      4830
           📷       0.14      0.17      0.16      1432
          🇺🇸       0.42      0.52      0.46      1949
           ☀       0.23      0.49      0.31      1265
           💜       0.23      0.06      0.10      1114
           😉       0.10      0.08      0.09      1306
           💯       0.21      0.20      0.20      1244
           😁       0.10      0.06      0.07      1153
           🎄       0.57      0.64      0.60      1545
           📸       0.26      0.15      0.19      2417
           😜       0.07      0.03      0.04      1010
           😂       0.33      0.47      0.39      4534
           💕       0.18      0.08      0.11      2605
           🔥       0.45      0.45      0.45      3716
           😊       0.09      0.07      0.08      1613
           😎       0.15      0.12      0.13      1996
           ✨       0.27      0.21      0.23      2749
           💙       0.19      0.10      0.13      1549
           😘       0.13      0.10      0.11      1175

    accuracy                           0.31     50000
   macro avg       0.24      0.24      0.23     50000
weighted avg       0.28      0.31      0.29     50000

Top-palabras por emoji¶

Otro experimento consistió en fijar un emoji y seleccionar aquellas palabras con mayor probabilidad de pertenecer a la clase correspondiente. Esto lo podemos hacer gracias a que Naive-Bayes aprende una distribución de probabilidad sobre el conjunto de las clases (emojis). Notar que estas palabras necesariamente son más frecuentes que el valor $min\_ df$, que acá es $10$. Algunos ejemplos importantes son los siguientes. Para más detalles ver clasificador1_en, donde mostramos el top 5 para cada emoji.

emoji palabra probabilidad
❤ kfodiaries 0.807
cityofbrotherlylove 0.770
loveofmylife 0.727
💙 bbn 0.652
itsaboy 0.683
foreverroyal 0.678
😘 smooches 0.588
kissy 0.544
smooch 0.541
🔥 instant_classic 0.778
flames 0.835
fuego 0.823
🎄 christmastree 0.846
ohchristmastree 0.845
xmas2016 0.828
🇺🇸 merica 0.919
ivoted 0.928
godblessamerica 0.912

Primeramente notar que hay ciertas referencias culturales, como lo son kfodiaries, que es un programa radial, y cityofbrotherlylove, que es una referencia a la ciudad de Philadelphia. Por otro lado loveofmylife es una expresión que se pudiese asociar de manera más directa al emoji ❤ (recordar que es el más común de todos).

El caso del emoji 😘 es más "amigable" para el clasificador, pues las palabras con mayor probabilidad son todas variaciones del término "besos". Podriamos pensar que entonces se está menos propenso al overfitting en este caso respecto del anterior, sin embargo los resultados de clasificación de este emoji muestran dificultad para clasificar tweets para esta clase de buena manera.

Otros casos interesantes incluyen palabras en otro idioma, como lo son "fuego" (en español) para el emoji 🔥. Por otro lado, palabras con probabilidadmuy grande están generalmente asociadas a emojis cuyo valor semántico está más alejado de corazones , como lo son el mismo 🔥, 🎄 y 🇺🇸. Para esto tenemos vocabualario que está asociado a las clases de manera muy explícita. Para el emoji 🇺🇸 tenemos en particular un vocabulario político y/o patriota que facilita la clasificación.

Visualización de tokens según Naive-Bayes¶

Esta seccion consiste en una visualizacion de los tokens segun la codificacion que nos entrega Naive Bayes. De la seccion anterior, se pueden obtener las probabilidad de que un token pertenezca a una clase dada. En nuestro caso, a un emoji dado. Esto es:

$$ P(w \in C) = \frac{\text{\#(tweets donde $w$ es uno de sus tokens y el tweet tiene el emoji $C$)}}{\text{\#(tweets con el token $w$)}} $$

De esta manera, cada token posee un vector de probabilidades. Donde la $C-$esima componente corresponde a $P(w \in C)$. Es decir,

$$\vec{w} = (P(w \in C) : \text{$C$ es un emoji})$$

En particular, cada vector $\vec{w}$ es uno con tantas coordenadas como emojis (20 en Ingles). Y cada coordenada esta entre 0 y 1. Es decir, cada $\vec{w} \in [0, 1]^{\text{\#Emojis}}$.

Ahora bien, es de nuestro interes visualizar cada token segun su vector de probabilidad. Sin embargo, es necesario reducir la dimensionalidad de cada vector a una facil de interpretar (en nuestro caso 2-dimensiones). Para esto, se utiliza un metodo de reduccion de dimensionalidad denominado UMAP y ampliamente utilizado para la visualizacion de datos en altas dimensiones.

Luego de reducir los vectores de probabilidad a uno de bi-dimensional, visualizaremos segun dos aspectos el espacio de tokens. Primero, se colorean los vectores segun el emoji con mayor probabilidad. Por ejemplo, si el token $happy$ tiene mayor probabilidad de estar en la clase $smile$, entonces se asocia este token con dicho emoji. La razon de esto es solo para simplificar el analisis. Segundo, existen tokens con probabilidades maximas mas grandes que otras, es decir, tokens asociados a un mismo emoji (segun el criterio anterior) que poseen probabilidades distintas de pertenecer a dicha clase. Para observar esto, se visualizan los token con puntos de diferente tamaño y proporcional a tal probabilidad.

In [ ]:
figura = fig_umap(clf,vectorizer,X_train_bow.shape[1])
figura.show(renderer="notebook")
OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.

Comentarios El top 5 de la seccion anterior se puede capturar con los primero cinco punto de mayor tamaño para un emoji dado. Tambien, se observa que la clase con mas puntos corresponde al emoji del corazon. Mismo emoji con mayor popularidad visto en la etapa de analisis de los datos. Se observan grupos diferenciados, pero que logran solaparse. Esta zona coincide con aquellos tokens con probabilidades uniformes de pertenecer a cada clase y/o con probabilidad maxima cercanas a 0.1.

Otros ejes de estudio para Naive Bayes¶

Eliminación de stopwords

La eliminación de stopwords es un paso usual en muchas aplicaciones de procesamiento de lenguaje natural. Si bien esto se realizó para la exploración de datos, finalmente no lo realizamos para implementar Naive-Bayes. La razones se mencionan a continuación:

  • Palabras muy frecuentes deberían distribuir uniforme con respecto a las clases de emojis.

    Articulos y otras palabras con poco valor semántico aparecerán frecuentemente en muchos tweets. Por ende la probabilidad de pertenecer a una clase u otra no será igual para todos los emojis. Por ende, para un tweet cualquiera, la decisión de pertenencia a una clase debería estar basada en aquellas palabras que si tienen tendencia a estar acompañadas de uno u otro emoji.

  • Eliminar stopwords reduce el tiempo de cómputo del método, pero no de manera tan significativa.

    Si tenemos menos palabras entonces tenemos un vocabulario más pequeño. Esto se traduce en una dimensionalidad del problema más pequeña. Los stopwords en general se identifican con una lista fija, disponible en bibliotecas de NLP como NLTK. Sin embargo decidimos enfretar este problema eliminando palabras poco frecuentes, donde eiminamos una cantidad de palabras mucho más grande para cualquier cota de frecuencia. Más aún, estas palabras poco frecuentes suelen traducirse en overfitting (comprobamos esto con el gridsearch), por ende atacamos al mismo tiempo otro problema.

Subsampling

A partir del subsampling, se puede observar que los resultados no presentan una mejorar e incluso en varias clases disminuye la efectividad del clasificador, por lo que es mejor no hacer el subsampling para obtener mejores metricas. En efecto, los resultados de entrenar el modelo con 9682 elementos elegidos al azar por clase, que corresponde a la frecuencia del emoji 😜, son los siguientes:

          precision    recall  f1-score   support

       ❤       0.45      0.10      0.16     10798
       😍       0.26      0.14      0.19      4830
       📷       0.13      0.20      0.16      1432
      🇺🇸       0.34      0.56      0.42      1949
       ☀       0.18      0.56      0.27      1265
       💜       0.09      0.12      0.10      1114
       😉       0.07      0.13      0.09      1306
       💯       0.15      0.30      0.20      1244
       😁       0.07      0.11      0.09      1153
       🎄       0.44      0.70      0.54      1545
       📸       0.21      0.21      0.21      2417
       😜       0.05      0.08      0.06      1010
       😂       0.39      0.28      0.32      4534
       💕       0.14      0.17      0.15      2605
       🔥       0.46      0.41      0.44      3716
       😊       0.08      0.10      0.09      1613
       😎       0.13      0.11      0.12      1996
       ✨       0.22      0.24      0.23      2749
       💙       0.11      0.15      0.13      1549
       😘       0.08      0.21      0.12      1175

accuracy                            0.22     50000
macro avg       0.20      0.24      0.21     50000
weighted avg    0.28      0.22      0.22     50000


Esto nos muestra que si bien las métricas de algunos emojis poco frecuentes mejoran, como lo es el caso de 💕, cuyo f1 aumenta de 0.11 a 0.15, en general los f1 son peores. Esto es más fuerte aún para emojis muy frecuentes como ❤. Para más detalles de implementación dirigirse a clasificador1_en.

Eventualmente se podría intentar un oversampling. Sin embargo, hay un riesgo fuerte de overfitting, pues tendríamos que repetir tweets que quizás tengan vocabulario muy específico que no refleje realmente el valor semántico del emoji.

Naive Bayes en Español¶

Realizaremos los pasos anteriores pero para el dataset en español. Recordemos que este es de menor tamaño que aquel en inglés. Primero veamos los resultados del gridsearch, cuyos detalles se encuentran en la sección clasificador1_es. Los valores a analizar fueron los mismos que en la grilla para inglés.

gridsearch

Parámetros escogidos:

  • alpha = 0.2
  • minimas ocurrencias para token = 5

Resultados de clasificación:

  • accuracy = 0.2735
  • macro f1 = 0.1592299196449812
  • weighted f1 = 0.2585226651076328

Es interesante notar que el valor de alpha es el mismo en ambos idiomas. Sin embargo, el número de mínimas ocurrencias óptimo cambia. Esto puede explicarse por la diferencia de tamaño de los datasets y las diferencias lingüisticas entre ambos idiomas.

In [ ]:
df_es_train = pickle.load(open(file_names["df_es_train"], "rb"))
df_es_test = pickle.load(open(file_names["df_es_test"], "rb"))

df_es_train['tokenized_text'] = df_es_train['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))
df_es_test['tokenized_text'] = df_es_test['text'].str.lower().apply(lambda x: " ".join(tt.tokenize(x)))

best_min_df = 5
best_alpha = 0.2

vectorizer = CountVectorizer(min_df=best_min_df)
X_train_bow = vectorizer.fit_transform(df_es_train["tokenized_text"])
X_test_bow = vectorizer.transform(df_es_test["tokenized_text"])

clf = MultinomialNB(alpha=best_alpha)
clf.fit(X_train_bow, df_es_train["label"])

y_pred_NB_es = clf.predict(X_test_bow)
dict_NB_es = classification_report(df_es_test["label"], y_pred_NB_es, target_names=df_es_mapping["emoji"],output_dict=True)
In [ ]:
print(classification_report(df_es_test["label"], y_pred_NB_es, target_names=df_es_mapping["emoji"]))
              precision    recall  f1-score   support

           ❤       0.36      0.39      0.37      2141
           😍       0.28      0.30      0.29      1408
           😎       0.13      0.11      0.12       339
           💙       0.13      0.04      0.06       413
           💜       0.10      0.03      0.05       235
           😜       0.11      0.06      0.08       274
           💞       0.00      0.00      0.00        93
           ✨       0.22      0.11      0.14       416
           🎶       0.13      0.19      0.16       212
           💘       0.05      0.02      0.03       134
           😁       0.05      0.04      0.04       209
           😂       0.44      0.55      0.49      1499
           💕       0.05      0.04      0.04       352
           😊       0.11      0.14      0.12       514
           😘       0.20      0.16      0.18       397
           💪       0.28      0.36      0.32       307
           😉       0.11      0.09      0.10       453
           👌       0.08      0.11      0.10       180
          🇪🇸       0.29      0.41      0.34       424

    accuracy                           0.27     10000
   macro avg       0.16      0.17      0.16     10000
weighted avg       0.25      0.27      0.26     10000

Los resultados son más bajos que inglés para gran parte de los emojis.

Top-palabras por emoji¶

Análogamente, obtenemos estas palabras al tomar aquellas con altas probabilidades de pertenecer a alguna clase.

emoji palabra probabilidad
😍 preciosidad 0.643
losamo 0.684
amordehermanos 0.626
✨ estrellas 0.393
brilla 0.349
mágicas 0.369
😂 meo 0.859
aburrimiento 0.713
parto 0.680
💪 empujón 0.693
altafit 0.670
gymlife 0.664
🇪🇸 hispanidad 0.785
vivaespaña 0.808
hiszpania 0.748

Hay ciertos efectos similares. Por ejemplo, 😍 muestra palabras que son fácilmente realcionadas con "enamoro". Esto es parecido al caso de ✨, donde tenemos palabras cuyo significado literal está muy cerca de aquel del emoji.

Es interesante notar las palabras comunes para 😂, donde se aprecio por un lado vocabulario coloquial, pero además sarcasmo, pues "aburrimiento" y "parto" parece ser el siginificado contrario a risas.

Por último notar la gran cantidad de palabras prestadas del inglés para el emoji 💪 y lo fuertemente asociado al patriotismo para las palabras del emoji 🇪🇸.

Visualización de tokens según Naive-Bayes¶

Análogamente a lo realizado en inglés, visualizamos lo aprendido por Naive-Bayes.

In [ ]:
figura = fig_umap(clf,vectorizer,X_train_bow.shape[1],df_es_mapping)
figura.show(renderer="notebook")

Transformers¶

Predicción de emojis y visualización¶

Los transformers son una arquitectura medianamente reciente que ha tenido espectaculares resultados en procesamiento de lenguaje natural. Su estructura está basada en capas de atención, que a su vez tienen varias "cabezas". A diferencia de como se usa en redes neuronales recurrentes, las atenciones en un transformer son la base principal de la representación final del input, además de poder ser entrenadas en paralelo, y por ende en mayores volúmenes de datos de modo más eficiente.

arquitectura

(fuente imagen)

Para este proyecto hemos querido comparar resultados de estas arquitecturas con un clasificador visto en el curso que también es apropiado para la tarea de predicción, como lo es Naive-Bayes. Más aún, se usaran vectores que corresponden a tweets extraidos de estos modelos, y se analizarán usando herramientas de clustering revisadas en el curso. Para comprender las nociones del funcionamiento de la arquitectura y como modela los elementos linguisticos, observemos de forma resumida como funciona un módulo de atención:

arquitectura

Una manera de visualizar la arquitectura es pensar en una matriz, donde cada elemento codifica la importancia de un elemento de la sequencia de input con respecto a la secuencia de output. En este ejemplo mostramos una representación idealizada de un módulo de auto-atención (sefl attention), que es el modo en que los codificadores basados en transformers procesan las sequencias. Un poco más formalmente, consideramos matrices $W$ que serán aprendidas durante el entrenamientom y se relacionan del modo siguiente:

arquitectura

Visualizando Atenciones¶

Si bien la capacidad de interpretación que tienen los modelos va bajando a medida que la magnitud de estos sube, es interesante tener acceso al interior de los modelos. Con la biblioteca bertviz podemos visualizar las capas de atención para cada cabeza y cada capa para un input dado.

In [ ]:
from transformers import AutoTokenizer, AutoModelForSequenceClassification, utils
from bertviz import model_view, head_view
import csv
utils.logging.set_verbosity_error()  # Suppress standard warnings
from config import rank_emojis_text, labels_es

Cargando del modelo

In [ ]:
MODEL = f"ccarvajal/beto-emoji"
folder = MODEL.replace('ccarvajal','modelos')

try:
    tokenizer = AutoTokenizer.from_pretrained(folder)
    model = AutoModelForSequenceClassification.from_pretrained(folder, output_attentions=True)
except ValueError:
    tokenizer = AutoTokenizer.from_pretrained(MODEL)
    tokenizer.save_pretrained(folder)
    model = AutoModelForSequenceClassification.from_pretrained(MODEL, output_attentions=True)
    model.save_pretrained(folder)
In [ ]:
ejemplo = "Tapas + sangria = @ Ocaña Barcelona"
rank_emojis_text(ejemplo,model,tokenizer,labels_es)
1) 😍 0.418
2) 🇪🇸 0.2464
3) 👌 0.097
4) 😊 0.0895
5) ❤ 0.0742
6) 😁 0.0254
7) 💙 0.0105
8) 😎 0.0093
9) 💜 0.0064
10) 💕 0.0048
11) 😉 0.004
12) 😜 0.0032
13) ✨ 0.0026
14) 💞 0.0025
15) 💘 0.0018
16) 😘 0.0016
17) 😂 0.0013
18) 💪 0.0008
19) 🎶 0.0007

Este ejemplo es uno de muchos que no está bien clasificado. El emoji en cuestión usado en el tweet es 🇪🇸 y no 😍.

Podemos usar este ejemplo para visualizar la capa de atención.

In [ ]:
def display_model_view(input_text):
    inputs = tokenizer.encode(input_text, return_tensors='pt')  # Tokenize input text
    outputs = model(inputs)  # Run model
    attention = outputs[-1]  # Retrieve attention from model outputs
    tokens = tokenizer.convert_ids_to_tokens(inputs[0])  # Convert input ids to token strings
    model_view(attention, tokens, include_layers=[11])
In [ ]:
display_model_view(ejemplo)
In [ ]:
def display_head_view(input_text):
    inputs = tokenizer.encode(input_text, return_tensors='pt')  # Tokenize input text
    outputs = model(inputs)  # Run model
    attention = outputs[-1]  # Retrieve attention from model outputs
    tokens = tokenizer.convert_ids_to_tokens(inputs[0])  # Convert input ids to token strings
    head_view(attention, tokens, layer=11)
In [ ]:
display_head_view(ejemplo)
Layer:

Un token que obviamos en el la representación idealizada es el token [CLS] (o < s > si la arquitectura de base es Roberta en vez de Bert). La representación de este token a través de la última capa es aquel que se usa como input para el clasificador, que es una capa feedforward con una función de activación. Al pasar el cursor por el token [CLS] a la izquierda podemos observar aquellos tokens que tienen más peso en el cómputo de aquella representación que se usará en la predicción.

En este caso, se observa una influencia más fuerte del token tapa, que es una comida típica de bar en Barcelona. Una hopótesis razonable es pensar que aquel token está "empujando" la clasificación hacia un emoji equivocado. Veamos lo que sucede cuando sólamente predecimos el emoji de la frase "tapas".

In [ ]:
rank_emojis_text("tapas",model,tokenizer,labels_es)
1) 😍 0.6418
2) ❤ 0.1665
3) 😊 0.0505
4) 👌 0.0341
5) 🇪🇸 0.0297
6) 💙 0.0228
7) 💜 0.0148
8) 💕 0.0143
9) 💘 0.0063
10) 💞 0.0045
11) 😁 0.0043
12) 😎 0.0039
13) ✨ 0.0028
14) 😘 0.0008
15) 😉 0.0008
16) 😜 0.0007
17) 😂 0.0006
18) 🎶 0.0004
19) 💪 0.0004

Se observa que el emoji 😍 es aquel con más probabilidad con un 64% de probabilidad, contra apenas 2,9% del token 🇪🇸.

Veamos otro ejemplo.

In [ ]:
otro_ejemplo = "Vamoooos!! #Gym #healthy @ Albacete Capital"
rank_emojis_text(otro_ejemplo,model,tokenizer,labels_es)
1) 💪 0.9921
2) 😁 0.0012
3) 😂 0.0011
4) 🇪🇸 0.0009
5) 👌 0.0007
6) 😜 0.0006
7) 😘 0.0006
8) 😉 0.0006
9) 😎 0.0004
10) 😊 0.0003
11) 💙 0.0003
12) 🎶 0.0003
13) 😍 0.0002
14) ❤ 0.0002
15) 💜 0.0001
16) ✨ 0.0001
17) 💘 0.0001
18) 💕 0.0001
19) 💞 0.0001

Acá la clasificación es correcta pues predice 💪 con alta probabilidad, que es efectivamente el label asociado. Observemos las capas de atención.

In [ ]:
display_model_view(otro_ejemplo)
In [ ]:
display_head_view(otro_ejemplo)
Layer:

Aquellos tokens con más influencia con respecto a [CLS] son gym, healthy y @. As interesante señalar que estas palabras son prestadas del inglés, y por ende sus tokens aparecen "cortados", puesto que el tokenizer está diseñado para español. Si hacemos una "españolización" de este mismo tweet obtenemos lo siguiente.

In [ ]:
otro_ejemplo = "Vamos!! #Gimnasio #saludable @ Albacete Capital"
rank_emojis_text(otro_ejemplo,model,tokenizer,labels_es)
1) 💪 0.9857
2) 😁 0.0023
3) 🇪🇸 0.0023
4) 😊 0.002
5) 😘 0.0015
6) 😉 0.0011
7) 👌 0.001
8) 😜 0.0009
9) 😎 0.0005
10) 💙 0.0005
11) 😂 0.0004
12) 🎶 0.0004
13) ✨ 0.0003
14) ❤ 0.0003
15) 💜 0.0002
16) 😍 0.0002
17) 💘 0.0002
18) 💕 0.0002
19) 💞 0.0002
In [ ]:
display_head_view(otro_ejemplo)
Layer:

La predicción sigue siendo la misma, además de que haya más influencia del token vamos que de @ en esta visualización.

Lo interesante de esto es observar como la arquitectura toma tokens y modela la relación entre ellos de modo que la comprensión que tiene de la frase sea consistente con su significado. Tener tokens cortados que provenian de otro idioma y tener vamos escrito de una manera más coloquial no fue impedimento para que la clasificación diera con el emoji correcto por un amplio margen.

Comparación de resultados de clasificación¶

Teniendo una noción básica de como los tranformers modelan el lenguaje, analicemos los resultados junto con aquellos de Naive-Bayes. Procesar los tweets con transformers puede ser costoso, es por esto que en las secciones hemos importado el modelo y hemos procesado su resultado en el test set:

  • clasificador2_BERTweet_en
  • clasificador2_transformer_en

Para ver detalles de aquello se envita a referirse a tales secciones. Importaremos esos resultados que hemos guardado en forma de archivo pickle.

In [ ]:
from os import listdir

modelos = [f.replace('.pickle','') for f in listdir('resultados_test/')]
print("Modelos disponibles:")
for m in modelos: print("\t- "+m)
Modelos disponibles:
	- beto-emoji
	- twitter-roberta-base
	- bertweet-base
In [ ]:
preds_transformer = {}
modelos.remove('beto-emoji')
for m in modelos:
    with open('resultados_test/{}.pickle'.format(m), 'rb') as handle:
        list_pred = pickle.load(handle)
        preds_transformer[m] = [str(i) for i in list_pred]
In [ ]:
dict_results = {}
for m in modelos:
    # print("Métricas de clasificación usando Transformers-{}".format(m))
    # print(classification_report(df_us_test["label"], preds_transformer[m], target_names=df_us_mapping["emoji"]))
    dict_results[m] = classification_report(df_us_test["label"], preds_transformer[m], target_names=df_us_mapping["emoji"],output_dict=True)

dict_results['Naive-Bayes'] = dict_NB

Modelos utilizados¶

Como se señaló en la planificación, su usaron modelos pre-entrenados. Más precisamente se usó:

  • BERTweet ajustado para predicción de emojis (CardiffNLP)
  • Twitter-roberta ajustado para predicción de emojis (CardiffNLP)

Estos están su vez basados en las arquitecturas BERTweet y Twitter-Roberta. Ambas son arquitecturas que heredan el procedimiento de Roberta, un modelo de lenguaje más general asociado a BERT, que es uno de los modelos más usados en procesamiento de lenguaje natural.

Resultados¶

Es esperable que los modelos muestren una mayor capacidad de clasificación respecto a un modelo más simple como Naive-Bayes. Veamos hasta que punto es cierta esta hipótesis.

In [ ]:
clfs = modelos + ['Naive-Bayes']
cols = pd.MultiIndex.from_product([clfs, ['precission', 'recall', 'f1-score']])

df_en = pd.DataFrame(columns=cols)
In [ ]:
for lab in df_us_mapping["emoji"].values:
  values = []
  for m in clfs:
    values += list(dict_results[m][lab].values())[:3]
  df_en.loc[lab] = values

df_en
Out[ ]:
twitter-roberta-base bertweet-base Naive-Bayes
precission recall f1-score precission recall f1-score precission recall f1-score
❤ 0.741124 0.862197 0.797089 0.812185 0.851824 0.831533 0.376373 0.482404 0.422843
😍 0.320503 0.443064 0.371948 0.327511 0.527950 0.404249 0.260080 0.243064 0.251284
📷 0.303647 0.697626 0.423126 0.308837 0.710196 0.430476 0.143106 0.171788 0.156141
🇺🇸 0.722648 0.532068 0.612884 0.815058 0.572088 0.672294 0.420067 0.517701 0.463801
☀ 0.713768 0.622925 0.665260 0.745455 0.583399 0.654545 0.228070 0.493281 0.311922
💜 0.344262 0.018851 0.035745 0.230769 0.013465 0.025445 0.228758 0.062837 0.098592
😉 0.161893 0.099541 0.123281 0.177606 0.070444 0.100877 0.101747 0.075804 0.086880
💯 0.337004 0.122990 0.180212 0.294221 0.270096 0.281643 0.208511 0.196945 0.202563
😁 0.143443 0.030356 0.050107 0.137681 0.049436 0.072750 0.103937 0.057242 0.073826
🎄 0.638710 0.768932 0.697797 0.650188 0.783172 0.710511 0.566016 0.638188 0.599939
📸 0.473282 0.025652 0.048666 0.394464 0.047166 0.084257 0.263504 0.149359 0.190652
😜 0.101449 0.013861 0.024390 0.250000 0.000990 0.001972 0.065817 0.030693 0.041864
😂 0.452065 0.533524 0.489428 0.419958 0.663652 0.514403 0.328016 0.467137 0.385406
💕 0.275835 0.136276 0.182425 0.280054 0.238772 0.257770 0.181971 0.082917 0.113924
🔥 0.577502 0.526372 0.550753 0.743255 0.444833 0.556566 0.453903 0.449139 0.451508
😊 0.135502 0.265964 0.179535 0.157165 0.189709 0.171910 0.087386 0.071296 0.078525
😎 0.185644 0.225451 0.203620 0.214162 0.246994 0.229409 0.148607 0.120240 0.132927
✨ 0.306567 0.419425 0.354224 0.342202 0.407057 0.371823 0.267382 0.208439 0.234260
💙 0.239531 0.092318 0.133271 0.340824 0.058748 0.100220 0.189633 0.096837 0.128205
😘 0.181388 0.195745 0.188293 0.180762 0.238298 0.205580 0.128894 0.102128 0.113960

Entre transformers y Naive-Bayes tenemos una mejora sustantiva de los resultados. Esto es particularmente cierto para emojis con frecuencia alta como lo son ❤ y 😍. Solamente algunos emojis presentan una mejor sensibilidad (recall) con Naive Bayes, pero esta mejora es marginalmente pequeña.

Resumen de resultados globales

In [ ]:
df_en_summary = pd.DataFrame(columns=['accuracy','macro f1','weighted f1','macro precision','weighted precision','macro recall','weighted recall'])

for m in clfs:
    values = []
    values += [dict_results[m]['accuracy']]
    values += [dict_results[m]['macro avg']['f1-score']] + [dict_results[m]['weighted avg']['f1-score']]
    values += [dict_results[m]['macro avg']['precision']] + [dict_results[m]['weighted avg']['precision']]
    values += [dict_results[m]['macro avg']['recall']] + [dict_results[m]['weighted avg']['recall']]
    df_en_summary.loc[m] = values

df_en_summary
Out[ ]:
accuracy macro f1 weighted f1 macro precision weighted precision macro recall weighted recall
twitter-roberta-base 0.46024 0.315603 0.431739 0.367788 0.452797 0.331657 0.46024
bertweet-base 0.48032 0.333912 0.456200 0.391118 0.486150 0.348414 0.48032
Naive-Bayes 0.30642 0.226951 0.287651 0.237589 0.284859 0.235872 0.30642

Los resultados anteriores reflejan lo comentado anteriormente. Tenemos diferencias sustanciales en todas las métricas, especialmente accuracy. Sin embargo, estamos particularmente interesados en la métrica macro f1, como se señaló y fundamentó en la planificación. En esta métrica BERTweet muestra un resultado competitivo con los mejores resultados de la competición original. En particular el ganador fue el equipo Tubingen-Oslo, que implementó SVM con bag-of-n-gramas tanto para palabras como para caracteres.

Comparación con ganador y baseline:

  • Tubingen-Oslo - 35.99
  • BERTweet - 33.39
  • twitter-roberta - 31.56
  • BASELINE - 30.98
  • Naive-Bayes - 22.69

Transformer en Español¶

No encontramos un modelo pre-entrenado para la predicción de emojis en tweets en español. Por ende decidimos ajustar uno nosotros mismos. Tomamos BETO, un modelo de lenguaje basado en la arquitectura BERT desarollado por el Departamento de Ciencias de la Computación de la Universidad de Chile.

Hemos dejado el modelo disponible para la comunidad a través de la biblioteca transformers en el siguiente enlace y le asignamos el nombre beto-emoji. Por otro lado, el ajuste y ejemplos de uso se pueden encontrar en este repositorio. Se realizaron 5 épocas (vueltas completas al dataset), lo cual es consistente con Twitter-Roberta. beto-emoji fue utilizado para la visualización que realizamos en la subsección anterior. Como antes, realizamos la inferencia de tweets del conjunto de test en el anexo clasificador2_transformers_es.

In [ ]:
%%capture
with open('resultados_test/beto-emoji.pickle', 'rb') as handle:
    list_pred = pickle.load(handle)
    preds_beto = [str(i) for i in list_pred]

dict_results = {}
dict_results['beto-emoji'] = classification_report(df_es_test["label"], preds_beto, target_names=df_es_mapping["emoji"],output_dict=True)

dict_results['Naive-Bayes'] = dict_NB_es
In [ ]:
clfs_es = ['beto-emoji','Naive-Bayes']
cols = pd.MultiIndex.from_product([clfs_es, ['precission', 'recall', 'f1-score']])

df_es = pd.DataFrame(columns=cols)
for lab in df_es_mapping["emoji"].values:
  values = []
  for m in clfs_es:
    values += list(dict_results[m][lab].values())[:3]
  df_es.loc[lab] = values

df_es
Out[ ]:
beto-emoji Naive-Bayes
precission recall f1-score precission recall f1-score
❤ 0.387432 0.434844 0.409771 0.355032 0.387202 0.370420
😍 0.289291 0.385653 0.330594 0.281461 0.295455 0.288288
😎 0.117647 0.106195 0.111628 0.130742 0.109145 0.118971
💙 0.360000 0.021792 0.041096 0.126984 0.038741 0.059369
💜 0.000000 0.000000 0.000000 0.100000 0.029787 0.045902
😜 0.035461 0.018248 0.024096 0.114286 0.058394 0.077295
💞 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
✨ 0.260204 0.122596 0.166667 0.218447 0.108173 0.144695
🎶 0.248756 0.235849 0.242131 0.133987 0.193396 0.158301
💘 0.000000 0.000000 0.000000 0.050847 0.022388 0.031088
😁 0.049296 0.033493 0.039886 0.050633 0.038278 0.043597
😂 0.509030 0.507672 0.508350 0.437433 0.545697 0.485604
💕 0.087432 0.045455 0.059813 0.046823 0.039773 0.043011
😊 0.117126 0.231518 0.155556 0.106129 0.138132 0.120034
😘 0.236041 0.234257 0.235145 0.197568 0.163728 0.179063
💪 0.366667 0.429967 0.395802 0.284987 0.364821 0.320000
😉 0.147002 0.167770 0.156701 0.108040 0.094923 0.101058
👌 0.088608 0.155556 0.112903 0.084746 0.111111 0.096154
🇪🇸 0.462830 0.455189 0.458977 0.293919 0.410377 0.342520

Tal como en antes, se notan mejoras evidentes respecto de Naive-Bayes, aunque ligeramente menos sustanciales con respecto a inglés. Para el emoji 💘 no se ven puntos del test set asignados a tal emoji, pero de todos modos las métricas que muestra con NB son de igual modo muy cercanas a 0.

In [ ]:
df_es_summary = pd.DataFrame(columns=['accuracy','macro f1','weighted f1','macro precision','weighted precision','macro recall','weighted recall'])

for m in clfs_es:
    values = []
    values += [dict_results[m]['accuracy']]
    values += [dict_results[m]['macro avg']['f1-score']] + [dict_results[m]['weighted avg']['f1-score']]
    values += [dict_results[m]['macro avg']['precision']] + [dict_results[m]['weighted avg']['precision']]
    values += [dict_results[m]['macro avg']['recall']] + [dict_results[m]['weighted avg']['recall']]
    df_es_summary.loc[m] = values

df_es_summary
Out[ ]:
accuracy macro f1 weighted f1 macro precision weighted precision macro recall weighted recall
beto-emoji 0.3050 0.181532 0.289702 0.198043 0.294545 0.188740 0.3050
Naive-Bayes 0.2735 0.159230 0.258523 0.164319 0.252616 0.165764 0.2735

La métrica macro f1 muestra una mejora que parece ser ligera, sin embargo hay que considerar nuevamente la magnitud del conjunto de entrenamiento respecto de la versión en inglés. Esto afectó a todos los equipos de la competencia, para los cuales Tubingen-Oslo fue nuevamente el de mejor resultados:

Comparación con ganador y baseline:

  • Tubingen-Oslo - 22.36
  • beto-emoji - 18.15
  • BASELINE - 16.72
  • Naive-Bayes - 15.92

Cabe señalar que fueron muchos los equipos que no superaron el BASELINE. Como trabajo futuro nos proponemos explorar otras aquitecturas, como lo era la utilización de n-gramas y SVM, que en conjunto lograron mejores resultados que una arquitectura de alta complejidad como Transformers.